W4. Операторы, выражения, структуры и объединения в C
1. Краткое содержание
1.1 Операторы (statements) в C
1.1.1 Что такое statement?
В C statement (оператор) — это законченная инструкция, которая указывает компьютеру выполнить действие. Каждый оператор в C заканчивается точкой с запятой (;). В общем случае операторы не «возвращают значение» как выражение: их роль — управлять потоком выполнения или вызвать side effect (побочный эффект), например изменить значение переменной.
1.1.2 Классы операторов
Операторы C удобно группировать так:
- Selection statements (операторы выбора): выбирают ветвь выполнения по условию.
if-else: выполняет блок при истинном условии и опциональныйelseпри ложном.switch: вычисляет целочисленное выражение и переходит кcaseс совпадающим значением. Обычно нуженbreak, иначе выполнение «проваливается» в следующийcase.
- Iteration statements (операторы цикла): повторяют блок, пока выполняется условие.
while: проверяет условие до каждой итерации тела.do-while: выполняет тело как минимум один раз, затем проверяет условие после итерации.for: компактно объединяет инициализацию, проверку и шаг после итерации.
- Jump statements (операторы перехода): безусловно передают управление.
break: выходит из ближайшего цикла илиswitch.continue: пропускает остаток текущей итерации цикла.return: завершает текущую функцию и опционально возвращает значение.goto: передаёт управление на метку в той же функции; обычно не рекомендуется из‑за читаемости и отладки.
- Compound statements (составные операторы, блоки): ноль или несколько операторов в
{}. Блок можно использовать там, где ожидается один оператор. Объявление внутри блока — тоже оператор. - Особые случаи:
- Expression statement (оператор-выражение): выражение и
;. Значение выражения отбрасывается; важен побочный эффект (присваиваниеa = b + c;, вызовprintf("hello");). - Declaration statement (оператор-объявление): вводит переменную (например
int x = 10;). В современном C объявления допускаются везде, где допускается оператор, не только в начале блока.
- Expression statement (оператор-выражение): выражение и
1.2 Выражения (expressions) в C
1.2.1 Что такое expression?
Expression (выражение) — это «формула»: сочетание переменных, констант, операторов и вызовов функций, которое при вычислении даёт одно значение. Почти каждое выражение в C имеет значение.
1.2.2 Строительные блоки выражений
Выражения строятся из элементов по уровню сложности:
- Primary expressions (первичные выражения): базовые элементы.
- Identifier: имя переменной или функции.
- Literal: константа (
123,0.01E-2,"string"). - Выражение в скобках:
(a + b).
- Postfix expressions (постфиксные): над первичными.
- Индекс массива:
arr[i+j]. - Вызов функции:
func(*p, 777). - Доступ к полю:
s.m(дляstruct/union) илиptr->m(указатель наstruct/union). - Постфиксные
++/--:x++,x--.
- Индекс массива:
- Unary expressions (унарные): один операнд.
- Префиксные
++/--:++x,--x. - Address-of (
&) и indirection (*):&x,*p. - Унарный
+/-:+x,-x. - Логическое НЕ (
!) и побитовое НЕ (~). sizeof: размер в байтах типа или объекта.
- Префиксные
- Binary expressions (бинарные): два операнда (
a + b,c * d). - Ternary expression (тернарное выражение): условный оператор
? :(condition ? value_if_true : value_if_false).
1.2.3 Precedence и associativity
- Precedence (приоритет): в сложном выражении задаёт порядок группировки операторов. Например,
*и/выше, чем+и-, поэтомуa + b * c— этоa + (b * c). - Associativity (ассоциативность): для операторов одного приоритета задаёт порядок слева/справа. Большинство бинарных — слева направо (
x - y + zкак(x - y) + z). Унарные операторы и присваивание — справа налево (x = y = 5какx = (y = 5)).
1.2.4 Side effects
Side effect (побочный эффект) — любое изменение состояния программы: запись в переменную, ввод-вывод и т.п. Выражения вроде x++ важны именно побочным эффектом: значением x++ является старое значение x, а побочный эффект — увеличение x.
1.3 Рекурсивные функции
Recursive function (рекурсивная функция) — функция, которая вызывает сама себя. Это естественно для задач, которые распадаются на похожие подзадачи меньшего размера.
1.3.1 Два обязательных компонента
Чтобы не зациклиться, у рекурсии должны быть:
- Base case (базовый случай): условие, при котором рекурсии нет — сразу возвращается ответ.
- Recursive step (рекурсивный шаг): вызов себя с аргументом, который приближает к базовому случаю.
1.3.2 Call stack при рекурсии
При каждом вызове функции на call stack (стек вызовов) кладётся кадр с её локальными переменными. При рекурсии на каждый самовызов добавляется новый кадр: стек растёт, пока не достигнут base case. Затем, когда каждый вызов возвращает результат своему вызывающему кадру, соответствующий кадр снимается (pop) — стек «разворачивается».
1.4 Структуры (struct)
Structure (структура, struct) — пользовательский тип, объединяющий связанные поля разных типов в одну логическую сущность.
1.4.1 Объявление и память
У каждого поля struct — своя область памяти. Полный размер — сумма размеров полей плюс memory padding (выравнивающие байты).
- Memory padding: компилятор вставляет «пустые» байты между полями для выравнивания на адреса, удобные для железа (например
intна границу 4 байт). Это ускоряет доступ, но увеличивает размер. - Packed structures: нестандартно (
__attribute__((packed))) убирает padding; экономит память, но может ухудшить производительность или быть небезопасным на некоторых архитектурах.
1.4.2 Доступ к полям
- Dot operator (
.): у значения структуры, напримерstudent.id. - Arrow operator (
->): у указателя на структуру, напримерstudentPtr->id; эквивалентно(*studentPtr).id.
1.5 Объединения (union)
Union (объединение, union) — тип, где все поля разделяют одну и ту же область памяти.
1.5.1 Общая память
Под union выделяется столько памяти, сколько нужно самому большому полю. Запись в одно поле может перетирать байты других, потому что это одни и те же байты. Это полезно для экономии памяти или type punning (интерпретации одних и тех же байт по-разному). Осмысленно в каждый момент времени использовать «активное» поле одно.
2. Определения
- Statement: законченная инструкция в C, заканчивается
;. - Expression: сочетание значений, переменных, операторов и функций, дающее одно значение при вычислении.
- Expression statement: выражение с
;, выполняется ради побочных эффектов. - Declaration statement: объявляет (и опционально инициализирует) новую переменную.
- Operator precedence: правила группировки операторов в выражении.
- Side effect: изменение состояния (переменная, I/O и т.д.).
- Recursion: приём, когда функция вызывает саму себя.
- Base case: условие остановки рекурсии.
- Structure (
struct): тип с полями разных типов; поля в отдельных местах памяти. - Union (
union): тип, где поля разделяют одну область памяти. - Memory padding: служебные байты внутри
structдля выравнивания. - Typedef: ключевое слово для псевдонима типа, часто для читаемости
struct/union. - Call stack: структура данных, хранящая информацию об активных подпрограммах (цепочке вызовов) программы.
3. Примеры
3.1. Разбор вывода: static и рекурсия (Лаба 4, Задание 1)
Каков ожидаемый вывод этой программы?
#include <stdio.h>
void func() {
static int x = 5;
int y = 5;
while (y < 10 && x < 10) {
printf("x = %d, y = %d\n", x, y);
x++;
y++;
func();
}
}
int main() {
func();
}Нажмите, чтобы увидеть решение
Проследим выполнение по шагам.
mainвызываетfunc().func(вызов 1):static int x = 5;выполняется один раз за всю программу:xсоздан и равен 5.int y = 5;локальнаяyдля этого вызова равна 5.- цикл
while: условие(y < 10 && x < 10)—(5 < 10 && 5 < 10), истина. printf:x = 5, y = 5xстановится 6 (статическийxтеперь 6).yстановится 6.- рекурсивный
func().
func(вызов 2):- строка
static int x = 5;пропускается,xостаётся 6. - новая локальная
y = 5. while:(5 < 10 && 6 < 10)— истина.printf:x = 6, y = 5x→ 7,y→ 6, рекурсия.
- строка
func(вызов 3):x= 7, новаяy= 5 → печатьx = 7, y = 5→x= 8, рекурсия.func(вызов 4):x= 8 →x = 8, y = 5→x= 9, рекурсия.func(вызов 5):x= 9 →x = 9, y = 5→x= 10, рекурсия.func(вызов 6):x= 10,y= 5 →(5 < 10 && 10 < 10)ложь, цикл не входит, возврат.- Возврат в вызов 5: у его локальной
yбыло 6, статическийx= 10 →(6 < 10 && 10 < 10)ложь, цикл завершается, возврат. - Аналогично «сворачивается» остальная рекурсия: условие
x < 10дальше нигде не выполняется, дополнительной печати нет.
Итоговый вывод:
x = 5, y = 5
x = 6, y = 5
x = 7, y = 5
x = 8, y = 5
x = 9, y = 53.2. Структуры студента и даты экзамена (Лаба 4, Задание 2)
Напишите программу с двумя структурами: student и exam_day. Первая содержит name, surname, group number и вложенную вторую структуру. Вторая — day, year и month экзамена. Поле month должно быть буквенным (например, May), не числом. Программа запрашивает все поля и печатает их.
Нажмите, чтобы увидеть решение
#include <stdio.h>
#include <string.h>
// Define the structure for the exam date.
struct exam_day {
int day;
char month[20]; // Character array to store the month's name
int year;
};
// Define the structure for the student.
// This structure contains another structure as one of its members.
struct student {
char name[50];
char surname[50];
int group_number;
struct exam_day exam_date; // Nested structure
};
int main() {
// Declare a variable of type 'student'.
struct student s1;
// --- Get User Input ---
printf("Enter student's first name: ");
scanf("%s", s1.name);
printf("Enter student's surname: ");
scanf("%s", s1.surname);
printf("Enter student's group number: ");
scanf("%d", &s1.group_number);
printf("Enter exam day (e.g., 22): ");
scanf("%d", &s1.exam_date.day);
printf("Enter exam month (e.g., October): ");
scanf("%s", s1.exam_date.month);
printf("Enter exam year (e.g., 2025): ");
scanf("%d", &s1.exam_date.year);
// --- Print the Stored Information ---
printf("\n--- Student Information ---\n");
printf("Name: %s\n", s1.name);
printf("Surname: %s\n", s1.surname);
printf("Group: %d\n", s1.group_number);
printf("Exam Date: %d %s %d\n", s1.exam_date.day, s1.exam_date.month, s1.exam_date.year);
return 0;
}3.3. «Шифрование» целого через union (Лаба 4, Задание 3)
С помощью union напишите программу: прочитать unsigned long long из консоли, затем «зашифровать», меняя местами каждый нечётный байт с соседним чётным, начиная со старшего байта. Должна быть функция encryption(...); напечатайте исходное, зашифрованное и расшифрованное значения.
Нажмите, чтобы увидеть решение
#include <stdio.h>
// A union allows storing different data types in the same memory location.
// Here, we can access the same 8 bytes of memory as either a single
// unsigned long long or as an array of 8 individual bytes (unsigned chars).
typedef union {
unsigned long long ull_value;
unsigned char bytes[8];
} ull_converter;
// Function to perform the byte-swapping encryption/decryption.
// The same logic works for both encryption and decryption.
void encryption(ull_converter *data) {
// An unsigned long long is 8 bytes. We swap pairs of bytes:
// bytes[0] with bytes[1]
// bytes[2] with bytes[3]
// bytes[4] with bytes[5]
// bytes[6] with bytes[7]
// The loop iterates 4 times for the 4 pairs.
for (int i = 0; i < 8; i += 2) {
// Use a temporary variable to swap the byte pair.
unsigned char temp = data->bytes[i];
data->bytes[i] = data->bytes[i+1];
data->bytes[i+1] = temp;
}
}
int main() {
// Create a union variable.
ull_converter data;
// Prompt user for input.
printf("Enter an unsigned long long integer: ");
// Read the value from the user. %llu is the format specifier for unsigned long long.
scanf("%llu", &data.ull_value);
// Print the original value.
printf("Original message: %llu\n", data.ull_value);
// Call the encryption function. We pass the address of the union.
encryption(&data);
// The bytes inside the union have been swapped. Reading the ull_value now gives the encrypted number.
printf("Encrypted message: %llu\n", data.ull_value);
// Call the function again. Swapping the swapped bytes returns them to their original positions.
encryption(&data);
// Print the decrypted message, which should match the original.
printf("Decrypted message: %llu\n", data.ull_value);
return 0;
}